package com.mlog.njyjy.ndop.share.sql.dao;

import cn.hutool.core.bean.copier.BeanCopier;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Model;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.plugin.activerecord.SqlPara;
import com.mlog.njyjy.ndop.share.sql.annotation.Column;
import com.mlog.njyjy.ndop.share.sql.annotation.Join;
import com.mlog.njyjy.ndop.share.sql.annotation.Transient;
import com.mlog.njyjy.ndop.share.sql.core.ModelDaoContainer;
import com.mlog.njyjy.ndop.share.sql.support.JoinFetchType;
import com.mlog.njyjy.ndop.share.sql.support.SqlOrmException;
import com.mlog.njyjy.ndop.share.sql.support.SqlSort;
import com.mlog.njyjy.ndop.share.sql.support.SqlTableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cglib.proxy.CallbackFilter;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.lang.NonNull;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.mlog.njyjy.ndop.share.sql.support.SqlHelper.pureAttrs2Fields;

/**
 * <p>
 * sql generic repository
 * </p>
 *
 * @author <a href="mailto:yingxiufeng@mlogcn.com">alex</a>
 * @version v1.0, 2020/7/8
 */
@SuppressWarnings("unchecked")
public abstract class BaseSqlRepository<T extends Model> implements SqlSimpleRepository<T, Object> {

    private static final Logger log = LoggerFactory.getLogger(BaseSqlRepository.class);

    private static final String FIELD_CREATE_AT = "createAt";

    private static final String FIELD_UPDATE_AT = "updateAt";

    protected final T dao;


    public BaseSqlRepository() {
        T instance = newInstance();
        if (instance != null) {
            Model<T> model = (Model) instance;
            dao = model.dao();
            ModelDaoContainer.INSTANCE.put(dao);
        } else {
            dao = null;
            throw new SqlOrmException("something is wrong which due to `dao` is null");
        }
    }

    /**
     * count the table
     *
     * @return
     */
    @Override
    public long count() {
        return Db.queryLong(StrUtil.format("select count(*) from {}", tblName()));
    }

    /**
     * count with where clause
     *
     * @param whereClause eg `where a>? and b<=?`
     * @param para
     * @return
     */
    @Override
    public long count(String whereClause, Object... para) {
        return Db.queryLong(StrUtil.format("select count(*) from {} {}", tblName(), whereClause), para);
    }

    /**
     * find first one
     *
     * @return
     */
    @Override
    public T findFirst(String sql) {
        if (StrUtil.isBlank(sql)) {
            sql = selectAllSql();
        }
        T entity = (T) dao.findFirst(sql.concat(" limit 1"));
        return attrs2Fields(entity);
    }

    /**
     * find top n
     *
     * @param limit
     * @return
     */
    @Override
    public List<T> findTopN(int limit, String sql) {
        List list = (List<T>) dao.find((StrUtil.isBlank(sql) ? selectAllSql() : sql).concat(" limit ?"), limit);
        list.forEach(entity -> attrs2Fields((T) entity));
        return list;
    }

    /**
     * TODO
     * find by sql & para
     *
     * @param sql
     * @param para
     * @return
     */
    @Override
    public List<T> find(String sql, Object... para) {
        List list = dao.find(sql, para);
        list.forEach(entity -> attrs2Fields((T) entity));
        return list;
    }

    /**
     * find by page
     *
     * @param pageNumber 页码
     * @param pageSize   分页大小
     * @return
     */
    @Override
    public Page<T> findByPage(int pageNumber, int pageSize) {
        return dao.paginate(pageNumber, pageSize, "select *", StrUtil.format("from {} order by id asc", tblName()));
    }

    /**
     * find by page with query sql
     *
     * @param pageNumber 页码
     * @param pageSize   分页大小
     * @param sql        查询sql
     * @param paras      sql参数
     * @return
     */
    @Override
    public Page<T> findByPage(int pageNumber, int pageSize, String sql, Object... paras) {
        SqlPara sqlPara = new SqlPara().setSql(sql);
        if (paras != null) {
            Arrays.stream(paras).forEach(sqlPara::addPara);
        }
        return dao.paginate(pageNumber, pageSize, sqlPara);
    }

    /**
     * find all
     *
     * @return
     */
    @Override
    public List<T> findAll(SqlSort sort) {
        List list = new ArrayList(1);
        if (sort == null) {
            list = dao.findAll();
        } else {
            list = dao.find(selectAllSql().concat(StrUtil.format(" ORDER by {} {}",
                    StrUtil.toUnderlineCase(sort.getField()),
                    sort.getOrder().name().toLowerCase())));
        }
        list.forEach(entity -> attrs2Fields((T) entity));
        return list;
    }

    /**
     * find all without sort
     *
     * @return
     */
    @Override
    public List<T> findAll() {
        return findAll(null);
    }

    /**
     * find one by id
     *
     * @param id
     * @return
     */
    @Override
    public T findOne(Object id) {
        T byId = (T) dao.findById(id);
        return attrs2Fields(byId);
    }


    /**
     * find one by some field
     *
     * @param fieldName
     * @param fieldValue
     * @return
     */
    @Override
    public List<T> findByField(String fieldName, Object fieldValue) {
        List list = dao.find(new SqlPara()
                .setSql(StrUtil.format("select * from {} where {}=? ", tblName(), StrUtil.toUnderlineCase(fieldName)))
                .addPara(fieldValue));
        list.forEach(entity -> attrs2Fields((T) entity));
        return list;
    }

    /**
     * delete one by id
     *
     * @param id
     * @return
     */
    @Override
    public boolean delete(Object id) {
        return dao.deleteById(id);
    }

    /**
     * insert
     *
     * @param entity
     * @return
     */
    @Override
    public boolean insert(T entity) {
        fields2Attrs(entity, Model.FILTER_BY_SAVE);
        return entity.save();
    }

    /**
     * insert bulk
     *
     * @param entity
     * @return
     */
    @Override
    public boolean insert(List<T> entity) {
        entity.forEach(e -> fields2Attrs(e, Model.FILTER_BY_SAVE));
        Db.batchSave(entity, batchSize());
        return true;
    }

    /**
     * update
     *
     * @param entity
     * @return
     */
    @Override
    public boolean update(T entity) {
        fields2Attrs(entity, Model.FILTER_BY_UPDATE);
        return entity.update();
    }


    /**
     * 数据库中实际表名
     *
     * @return
     */
    public String tblName() {
        return SqlTableUtil.getTblName(modelClass());
    }

    /**
     * 批量的大小 默认一次 100 条记录
     * 子类可覆盖修改
     *
     * @return
     */
    public int batchSize() {
        return 100;
    }

    /**
     * source type of <T>
     *
     * @return
     */
    public Class<T> modelClass() {
        return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    public String createDateFieldName() {
        return FIELD_CREATE_AT;
    }

    public String updateDateFieldName() {
        return FIELD_UPDATE_AT;
    }


    /**
     * select all
     *
     * @return
     */
    private String selectAllSql() {
        return StrUtil.format("select * from {} ", tblName());
    }

    /**
     * new instance
     *
     * @return
     */
    private T newInstance() {
        Class<T> aClass = modelClass();
        T ifPossible = ReflectUtil.newInstanceIfPossible(aClass);
        if (ifPossible != null) {
            return ifPossible;
        }
        return null;
    }

    /**
     * 将model中的attrs --> class 中的 fields
     * 发生在sql查询之后
     *
     * @param entity model entity
     * @return
     */
    public T attrs2Fields(T entity) {
        if (entity == null) {
            return null;
        }
        pureAttrs2Fields(entity, modelClass());
        Field[] fields = ReflectUtil.getFieldsDirectly(modelClass(), false);
        List<Field> joinFields = Arrays.stream(fields).filter(field -> field.getAnnotation(Join.class) != null).collect(Collectors.toList());
        List<Field> lazyFields = new ArrayList<>(1);

        joinFields.forEach(field -> {
            Join join = field.getAnnotation(Join.class);
            if (JoinFetchType.lazy.equals(join.fetchType())) {
                lazyFields.add(field);
            } else {
                // 立即赋值
                Object joinValue = fetchJoinValue(entity, field);
                if (joinValue != null) {
                    ReflectUtil.setFieldValue(entity, field, joinValue);
                }
            }
        });

        if (lazyFields.size() > 0) {
            List<String> fieldNames = lazyFields.stream().map(Field::getName).collect(Collectors.toList());
            // 创建代理
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(modelClass());
            enhancer.setCallback((MethodInterceptor) (proxy, method, args, methodProxy) -> {
                Optional<Field> optional = lazyFields.stream().filter(field -> field.getName().equalsIgnoreCase(method.getName().replace("get", "")))
                        .findFirst();
                return optional.map(field -> fetchJoinValue(entity, field)).orElse(null);
            });
            // 只代理join字段的get方法
            enhancer.setCallbackFilter(method -> fieldNames.contains(method.getName().replace("get", "")) ? 1 : 0);
            T proxy = (T) enhancer.create();
            Arrays.stream(ReflectUtil.getFieldsDirectly(modelClass(), false)).forEach(field -> {
                if (!"serialVersionUID".equalsIgnoreCase(field.getName())) {
                    ReflectUtil.setFieldValue(proxy, field, ReflectUtil.getFieldValue(entity, field));
                }
            });
            return proxy;
        }

        return entity;
    }

    private Object fetchJoinValue(T entity, Field joinField) {
        Class<?> joinedClazz = getClassTypeWithCollection(joinField);
        Join join = joinField.getAnnotation(Join.class);
        String joinedBy = join.joinedBy();
        String mappedBy = join.mappedBy();
        Object joinedValue = entity.get(StrUtil.toUnderlineCase(joinedBy));
        String sql = String.format("select * from %s where %s=?",
                SqlTableUtil.getTblName(joinedClazz),
                StrUtil.toUnderlineCase(mappedBy));
        if (List.class.equals(joinField.getType()) || Set.class.equals(joinField.getType())) {
            // TBD: set?
            List<Model> models = ModelDaoContainer.INSTANCE.get((Class<Model>) joinedClazz)
                    .find(sql, joinedValue);
            if (models.size() > 0) {
                return models.stream().map(m -> pureAttrs2Fields(m, (Class<? super Model>) joinedClazz)).collect(Collectors.toList());
            }
            return null;
        } else {
            Model model = ModelDaoContainer.INSTANCE.get((Class<Model>) joinedClazz)
                    .findFirst(sql, joinedValue);
            pureAttrs2Fields(model, (Class<? super Model>) joinedClazz);
            return model;
        }

    }

    private Class<?> getClassTypeWithCollection(Field field) {
        field.setAccessible(true);
        Class<?> type = field.getType();
        if (List.class.equals(type) || Set.class.equals(type)) {
            Type genericType = field.getGenericType();
            ParameterizedType pt = (ParameterizedType) genericType;
            return (Class<?>) pt.getActualTypeArguments()[0];
        }
        return type;
    }

    /**
     * 将实体类中的字段值---> model 的 attrs 中
     * 发生在 insert/update 之前
     *
     * @param entity
     * @param flag   insert or update
     */
    private void fields2Attrs(T entity, int flag) {
        Field[] fields;
        Class<?> superclass = modelClass().getSuperclass();
        if (superclass.isAssignableFrom(Model.class)) {
            fields = ReflectUtil.getFieldsDirectly(modelClass(), false);
        } else {
            fields = ReflectUtil.getFieldsDirectly(modelClass(), true);
        }
        List<Field> targetFields = Arrays.stream(fields)
                .filter(field -> field.getAnnotation(Transient.class) == null &&
                        field.getAnnotation(Join.class) == null)
                .collect(Collectors.toList());

        switch (flag) {
            case Model.FILTER_BY_SAVE:
                targetFields.stream()
                        .filter(new InsertablePredicate())
                        .forEach(field -> {
                            Object fieldValue = createDateFieldName().equals(field.getName()) ? getDateLikeFieldValue(field)
                                    : ReflectUtil.getFieldValue(entity, field);
                            if (fieldValue != null && !"serialVersionUID".equalsIgnoreCase(field.getName())) {
                                entity.set(StrUtil.toUnderlineCase(field.getName()), fieldValue);
                            }
                        });
                break;
            case Model.FILTER_BY_UPDATE:
                targetFields.stream().filter(new UpdatablePredicate())
                        .forEach(field -> {
                            Object fieldValue = updateDateFieldName().equals(field.getName()) ? getDateLikeFieldValue(field)
                                    : ReflectUtil.getFieldValue(entity, field);
                            if (fieldValue != null && !"serialVersionUID".equalsIgnoreCase(field.getName())) {
                                entity.set(StrUtil.toUnderlineCase(field.getName()), fieldValue);
                            }
                        });
                break;
            default:
                break;
        }
    }

    private Object getDateLikeFieldValue(@NonNull Field field) {
        Class<?> fieldType = field.getType();
        if (fieldType.isAssignableFrom(LocalDate.class)) {
            return LocalDate.now();
        } else if (fieldType.isAssignableFrom(LocalDateTime.class)) {
            return LocalDateTime.now();
        } else if (fieldType.isAssignableFrom(Date.class)) {
            return new Date();
        } else if (long.class.equals(fieldType) || Long.class.equals(fieldType)) {
            return LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();
        } else {
            log.warn("[sql-orm] you should check the field type of [{}], is it a date-like ?", field.getName());
            return null;
        }

    }

    static class InsertablePredicate implements Predicate<Field> {

        @Override
        public boolean test(Field field) {
            Column column = field.getAnnotation(Column.class);
            return column == null || column.insertable();
        }
    }

    static class UpdatablePredicate implements Predicate<Field> {
        @Override
        public boolean test(Field field) {
            Column column = field.getAnnotation(Column.class);
            return column == null || column.updatable();
        }
    }


}
